package com.maiko.usercenter.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.maiko.usercenter.constant.RedisKeyConstants;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

/**
 * @author: Maiko7
 * @create: 2025-07-15 9:06
 */
public class CacheClient {
    private final StringRedisTemplate stringRedisTemplate;

    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /**
     * Spring 负责管理 bean 及其依赖关系。当创建 CacheClient 的实例时，Spring 会在应用程序上下文中查找类型为
     * StringRedisTemplate 的 bean。如果找到该类型的 bean（通常在配置或其他组件中定义），
     * Spring 会通过构造函数将其注入到 CacheClient 实例中。
     */
    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 将任意Java对象序列化为json并存储在string类型的key中，并且可以设置TTL过期时间
     * @param key 比如：商品就是商品id，店铺就是店铺id
     * @param value 任意对象
     * @param time 过期时间
     * @param unit 单位：比如second
     */
    public void set(String key, Object value, Long time, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }

    /**
     * 将任意Java对象序列化为json并存储在string类型的key中，并且可以设置逻辑过期时间，用于处理缓存击穿问题
     * @param key   比如就是商品id这种
     * @param value 任意对象
     * @param time 过期时间
     * @param unit  单位
     */
    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
        // 设置逻辑过期
        RedisData redisData = new RedisData();
        redisData.setData(value);
        /**
         * plusSeconds加时间 unit不确定所以要把它转成秒unit.toSeconds(time)
         * 就是你当前的时间加上过期的时间
         */
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        // 写入Redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    /**
     * 根据指定的key查询缓存，并反序列化为指定类型，利用缓存空值的方式解决缓存穿透问题
     * @param keyPrefix 你比如user_1897037  user_就是keyPrefix
     * @param id id就是上面的1897037 ID类型。因为你不确实一定是long类型你不能写死，要写成泛型，泛型可以随便写 R ID
     *           或者随便你取 你取Maiko都行
     * @param type
     * @param dbFallback 有参有返回值的函数就是function
     * @param time 过期时间，时间你肯定不能写死，你写死难道大家都是分钟都是秒吗？
     * @param unit
     * @param <R>
     * @param <ID>
     * @return R 返回值类型不确定，你不能指定为shop那这个提取出来的方法就没意义了
     *
     * 就是这种泛型的都是先申明这个泛型参数然后再写返回值类型
     *  <R, ID> 是泛型参数，表示该方法可以接受两种类型的泛型参数。
     * R 表示返回结果的类型。
     *
     * Class<R> type 这个就是你告诉我是什么类型我才知道R是什么类型，泛型的推断。比如我直接传一个Shop.class
     * Function<ID, R> 有参有返回的函数就叫Function。ID是参数类型，R是返回类型
     */
    public <R,ID> R queryWithPassThrough(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){
        String key = keyPrefix + id;
        // 1.从redis查询商铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        /**
         * StrUtil.isNotBlank()这个方法只判断有数据才为true，
         * null 和 "" 都是false
         */
        if (StrUtil.isNotBlank(json)) {
            // 3.存在且不为空值，直接返回
            /**
             * 以前是
             * return JSONUtil.toBean(json,Shop.class);
             * 现在相当于Shop.class 就是type 学学这个思维
             */
            return JSONUtil.toBean(json, type);
        }
        /**
         * 上面已经判断了有值的情况，那你这里就要判断其他两个情况
         * 即为null和为"",那你不为null的时候就代表是"" 你为空那就是没有嘛
         * 没有的话别人访问你不直接返回null嘛返回null就是redis没数据。
         * 我问你你除了返回return Shop 你还能返回什么？只有null了啊。
         */
        // 判断命中的是否是空值
        if (json != null) {
            // 返回一个错误信息
            return null;
        }

        // 4.不存在，根据id查询数据库
        /**
         * 以前是getByID(id) 你这里你根本不确定是什么你不能写死，因为你getByID(id)
         * 你说你get的是User表还是Shop表？你不知道啊，
         * apply是Funtion里面的方法有参有返回值，相当于在做函数式编程
         */
        R r = dbFallback.apply(id);
        // 5.不存在，返回错误
        if (r == null) {
            /**
             * 数据库都不存在的时候，为了防止击穿，把null写入到redis中
             */
            // 将空值写入redis
            stringRedisTemplate.opsForValue().set(key, "", RedisKeyConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            // 返回错误信息
            return null;
        }
        // 6.存在，写入redis
        /**
         * 数据库存在这个数据，则把这个数据库中的新值写入redis
         * 在Java中，this 关键字用于引用当前对象实例。可以使用 this 调用本类的非构造方法，
         * 包括普通方法、静态方法、成员变量等。
         */
        this.set(key, r, time, unit);
        return r;
    }

    /**
     * 根据指定的key查询缓存，并反序列化为指定类型，需要利用逻辑过期解决缓存击穿问题
     */
    public <R, ID> R queryWithLogicalExpire(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
        String key = keyPrefix + id;
        // 1.从redis查询商铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isBlank(json)) {
            // 3.不存在，直接返回
            return null;
        }
        // 4.存在，需要先把json反序列化为对象
        /**
         * 相当于查到了String就是一个json字符串，你自己看redis就知道
         * 然后它是把json转成RedisData对象。就是包含比如Shop和过期时间
         *
         * 字符串转对象用toBean方法，对象转对象也用toBean方法
         */
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        /**
         * 这里相当于把Shop提取出来
         */
        R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
        LocalDateTime expireTime = redisData.getExpireTime();
        // 5.判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())) {
            // 5.1.未过期，直接返回店铺信息
            return r;
        }
        // 5.2.已过期，需要缓存重建
        // 6.缓存重建
        // 6.1.获取互斥锁
        String lockKey = RedisKeyConstants.USER_CACHE_REBUILD_LOCK + id;
        boolean isLock = tryLock(lockKey);
        // 6.2.判断是否获取锁成功
        if (isLock){
            // 6.3.成功，开启独立线程，实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    // 查询数据库
                    R newR = dbFallback.apply(id);
                    // 重建缓存
                    this.setWithLogicalExpire(key, newR, time, unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    // 释放锁
                    unlock(lockKey);
                }
            });
        }
        // 6.4.返回过期的商铺信息
        return r;
    }

    /**
     * 互斥锁解决缓存击穿
     */
    public <R, ID> R queryWithMutex(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
        String key = keyPrefix + id;
        // 1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            // 3.存在，直接返回
            /**
             * 用于将 JSON 字符串转换为 Java 对象。
             * shopJson 是要转换的 JSON 字符串，type 是要转换成的目标 Java 对象的类型。
             */
            return JSONUtil.toBean(shopJson, type);
        }
        /**
         * 你想啊如果是空值的话就不会击穿
         */
        // 判断命中的是否是空值
        if (shopJson != null) {
            // 返回一个错误信息
            return null;
        }

        // 4.实现缓存重建
        // 4.1.获取互斥锁
        String lockKey = RedisKeyConstants.USER_CACHE_REBUILD_LOCK + id;
        /**
         * 这一步就好比 Shop shop = null;
         * R就是Shop类，r就是shop变量
         */
        R r = null;
        try {
            boolean isLock = tryLock(lockKey);
            // 4.2.判断是否获取成功
            if (!isLock) {
                // 4.3.获取锁失败，休眠并重试
                Thread.sleep(50);
                return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
            }
            // 4.4.获取锁成功，根据id查询数据库
            r = dbFallback.apply(id);
            // 5.不存在，返回错误
            if (r == null) {
                /**
                 * 这里同理把空值写进入，起码不会缓存穿透，你不写进去的话，那代表数据库没有这个数据
                 * 如果别人还请求，你还是缓存没有数据库没有一直打进数据库。
                 */
                // 将空值写入redis
                stringRedisTemplate.opsForValue().set(key, "", RedisKeyConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                // 返回错误信息
                return null;
            }
            // 6.存在，写入redis
            this.set(key, r, time, unit);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            // 7.释放锁
            unlock(lockKey);
        }
        // 8.返回
        return r;
    }

    private boolean tryLock(String key) {
        /**
         * 它这里的value = 1 就是setnx lock 1 相当于它在redis图形界面设置了一个锁就是1
         * stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
         * 这行代码就是这个意思 没有1的锁的话我就设置一个锁，这个锁就是key就是店铺id这种
         *
         * SET key "1" NX EX 10 这就是String的分布式锁
         */
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        // 你这样直接flag它会拆箱,拆箱可能空指针,所以用一个胡图的工具类
        return BooleanUtil.isTrue(flag);
    }

    private void unlock(String key) {
        //相应的Redis命令是 DEL key
        stringRedisTemplate.delete(key);
    }
}
